﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls.WebParts;
using System.Xml;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.WebPartPages;

namespace Trentacular.SharePoint.Util
{
    public enum FormTypes
    {
        Display,
        Edit
    }

    public static class SPUtils
    {
        //private static readonly ILog Log = LoggingStaticContext.GetDefaultLogger(typeof(SPUtils));

        /// <summary>
        /// Performs an action using an SPWeb object that requires unsafe updates
        /// </summary>
        public static void DoUnsafeUpdate(this SPWeb web, Action action)
        {
            bool allowUnsafeUpdates = web.AllowUnsafeUpdates;
            web.AllowUnsafeUpdates = true;
            action();
            web.AllowUnsafeUpdates = allowUnsafeUpdates;
        }

        public static void BypassCatchAccessDenied(Action action)
        {
            bool originalCatchValue = SPSecurity.CatchAccessDeniedException;
            SPSecurity.CatchAccessDeniedException = false;
            try
            {
                action();
            }
            finally
            {
                SPSecurity.CatchAccessDeniedException = originalCatchValue;
            }
        }

        public static void DoWithLimitedWebPartManager(this SPFile file, Action<SPLimitedWebPartManager> action)
        {
            using (var webPartManager = file.GetLimitedWebPartManager(PersonalizationScope.Shared))
            {
                try
                {
                    action(webPartManager);
                }
                finally
                {
                    webPartManager.Web.Dispose();
                }
            }
        }

        #region Impersonation Helpers

        /// <summary>
        /// Performs an action using an SPSite object impersonating the SystemUser for the site
        /// </summary>
        public static void DoAsSystemUser(this SPSite site, Action<SPSite> action)
        {
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                DoAsUser(site, site.SystemAccount, action);
            });
        }

        public static SPUser GetSystemUser(this SPSite site, bool throwUnauthorizedAccessException)
        {
            SPUser user = null;
            BypassCatchAccessDenied(delegate()
            {
                try
                {
                    user = site.SystemAccount;
                }
                catch (UnauthorizedAccessException e)
                {
                    // Log.Error(string.Format("Error getting the System Account for site at '{0}'", site.Url), e);
                    if (throwUnauthorizedAccessException)
                        throw e;
                }
            });
            return user;
        }

        /// <summary>
        /// Performs an action using an SPWeb object impersonating the SystemUser for the site
        /// </summary>
        public static void DoAsSystemUser(this SPWeb web, Action<SPWeb> action)
        {
            var systemUser = GetSystemUser(web.Site, false);
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                // First check if we have permissions to read the SystemAccount from the Site
                if (systemUser != null)
                {
                    DoAsUser(web, systemUser, action);
                }
                else
                {
                    // We do not have permissions to read the SystemAccount, so we must
                    // construct a new SPSite no matter what using elevated priveleges
                    using (var systemSite = new SPSite(web.Url))
                    {
                        using (var systemWeb = systemSite.OpenWeb(web.ID))
                        {
                            action(systemWeb);
                        }
                    }
                }
            });
        }

        /// <summary>
        /// Performs an action using an SPWeb object impersonating a given user and
        /// allowing unsafe updates
        /// </summary>
        public static void DoUnsafeUpdateAsSystemUser(this SPWeb web, Action<SPWeb> action)
        {
            web.DoAsSystemUser(delegate(SPWeb systemWeb)
            {
                systemWeb.DoUnsafeUpdate(delegate()
                {
                    action(systemWeb);
                });
            });
        }

        /// <summary>
        /// Performs an action using an SPSite object impersonating a given user
        /// </summary>
        public static void DoAsUser(this SPSite site, SPUser user, Action<SPSite> action)
        {
            DoAsUser(site, user, delegate(SPSite impersonatedSite, bool isNewSite)
            {
                action(impersonatedSite);
            });
        }

        /// <summary>
        /// Performs an action using an SPWeb object impersonating a given user
        /// </summary>
        public static void DoAsUser(this SPWeb web, SPUser user, Action<SPWeb> action)
        {
            DoAsUser(web.Site, user, delegate(SPSite impersonatedSite, bool isNewSite)
            {
                if (!isNewSite)
                {
                    action(web);
                    return;
                }

                using (var impersonatedWeb = impersonatedSite.OpenWeb(web.ID))
                {
                    action(impersonatedWeb);
                }
            });
        }

        private delegate void SPSiteActionInternal(SPSite site, bool isNewSite);
        private static void DoAsUser(SPSite site, SPUser user, SPSiteActionInternal action)
        {
            // If we are already running as the given User Token, just pass the web along
            if (site.UserToken != null && site.UserToken.CompareUser(user.UserToken))
            {
                action(site, false);
                return;
            }

            string loginName = user.LoginName;
            //Log.InfoFormat("Opening site at '{0}' as user '{1}'", site.Url, loginName);
            using (var impersonatedSite = new SPSite(site.ID, user.UserToken))
            {
                action(impersonatedSite, true);
            }
            //Log.InfoFormat("Disposed site at '{0}' opened as user '{1}'", site.Url, loginName);
        }

        #endregion

        #region PropertyBag Helpers

        /// <summary>
        /// Simply adds a key/value pair to the SPWeb AllProperties Hashtable and the
        /// Deprecated SPWeb Property bag without updating
        /// </summary>
        public static void AddProperty(this SPWeb web, string key, string value)
        {
            web.Properties[key] = value;
            web.AllProperties[key] = value;
        }

        /// <summary>
        /// Simply removes the property with the given key from the SPWeb AllProperties
        /// hashtable and the deprecated SPWeb property bag without updating
        /// </summary>
        public static void RemoveProperty(this SPWeb web, string key)
        {
            web.AllProperties.Remove(key);
            web.Properties[key] = null;
        }

        /// <summary>
        /// Adds the given key/value pair to both the SPWeb AllProperties Hashtable and
        /// the deprecated SPWeb Property Bag and Updates both the Property Bag and the SPWeb.
        /// This method ensures the operation is done with System permissions.
        /// </summary>
        public static void AddPropertyAndUpdate(this SPWeb web, string key, string value)
        {
            var properties = new Dictionary<string, string>(1);
            properties.Add(key, value);
            AddPropertiesAndUpdate(web, properties);
        }

        /// <summary>
        /// Adds the given key/value pairs to both the SPWeb AllProperties Hashtable and
        /// the deprecated SPWeb Property Bag and Updates both the Property Bag and the SPWeb.
        /// This method ensures the operation is done with System permissions.
        /// </summary>
        public static void AddPropertiesAndUpdate(this SPWeb web, IDictionary<string, string> properties)
        {
            DoUnsafeUpdateAsSystemUser(web, delegate(SPWeb systemWeb)
            {
                foreach(var pair in properties)
                {
                    AddProperty(systemWeb, pair.Key, pair.Value);
                }

                systemWeb.Update();
                systemWeb.Properties.Update();
            });
        }

        /// <summary>
        /// Removes the property with the given key from both the SPWeb AllProperties
        /// hashtable and the deprecated SPWeb Property Bag and updates both the Property Bag
        /// and the SPWeb.  This method ensures the operation is done with System permissions.
        /// </summary>
        public static void RemovePropertyAndUpdate(this SPWeb web, string key)
        {
            RemovePropertiesAndUpdate(web, key);
        }

        /// <summary>
        /// Removes the properties with the given keys from both the SPWeb AllProperties
        /// hashtable and the deprecated SPWeb Property Bag and updates both the Property Bag
        /// and the SPWeb.  This method ensures the operation is done with System permissions.
        /// </summary>
        public static void RemovePropertiesAndUpdate(this SPWeb web, params string[] keys)
        {
            DoUnsafeUpdateAsSystemUser(web, delegate(SPWeb systemWeb)
            {
                foreach (var key in keys)
                {
                    RemoveProperty(systemWeb, key);
                }

                systemWeb.Update();
                systemWeb.Properties.Update();
            });
        }

        /// <summary>
        /// Safely gets a property value from the SPWeb Property Bag
        /// </summary>
        public static string GetPropertyValue(this SPWeb web, string key)
        {
            if (web.AllProperties.ContainsKey(key))
                return web.AllProperties[key].ToString();

            return null;
        }

        /// <summary>
        /// Reads a property value from the Farms property bag with elevated priveleges
        /// </summary>
        public static string GetFarmProperty(string key)
        {
            return GetFarmProperty<string>(key);
        }

        /// <summary>
        /// Reads a property value from the Farms property bag with elevated priveleges
        /// </summary>
        /// <typeparam name="T">T can be a string, DateTime, or an int</typeparam>
        public static T GetFarmProperty<T>(string key)
        {
            T value = default(T);
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                var farmProperties = SPFarm.Local.Properties;
                if (farmProperties.ContainsKey(key))
                    value = (T)farmProperties[key];
            });
            return value;
        }

        #endregion

        #region List Helpers

        public static SPList GetListByName(this SPWeb web, string listName, bool throwIfNotFound)
        {
            try
            {
                return web.Lists[listName];
            }
            catch (ArgumentException e)
            {
                if (throwIfNotFound)
                    throw new ArgumentException("Failed to find list with name '" + listName + "'", "listName", e);
            }

            return null;
        }

        public static SPList GetListById(this SPWeb web, Guid listId, bool throwIfNotFound)
        {
            try
            {
                return web.Lists[listId];
            }
            catch (ArgumentException e)
            {
                if (throwIfNotFound)
                    throw new ArgumentException("Failed to find list with id: '" + listId.ToString() + "'", "listId", e);
            }

            return null;
        }

        public static SPList GetListByIdOrName(this SPWeb web, Guid? listId, string listName, bool throwIfNotFound)
        {
            if (listId.HasValue)
                return GetListById(web, listId.Value, throwIfNotFound);

            return GetListByName(web, listName, throwIfNotFound);
        }

        /// <summary>
        /// Gets an SPList provided its list id or name and throws an exception if not found
        /// </summary>
        public static SPList GetListByIdOrName(this SPWeb web, Guid? listId, string listName)
        {
            return GetListByIdOrName(web, listId, listName, true);
        }

        private static object ProvisionLock = new object();
        /// <summary>
        /// Gets a list by name or creates the list if it does not exist.  If the list
        /// needs to be created, the System User is impersonated to create and execute the
        /// onListCreated event.
        /// </summary>
        public static SPList GetOrCreateList(this SPWeb web, string listName, string description, SPListTemplateType listTemplateType, Action<SPList> onListCreated)
        {
            lock (ProvisionLock)
            {
                var list = GetListByName(web, listName, false);
                if (list != null)
                    return list;

                DoAsSystemUser(web, delegate(SPWeb systemWeb)
                {
                    systemWeb.AllowUnsafeUpdates = true;
                    var listId = systemWeb.Lists.Add(listName, description, SPListTemplateType.DocumentLibrary);
                    list = systemWeb.Lists[listId];

                    if (onListCreated != null)
                        onListCreated(list);
                });

                return list;
            }
        }

        #endregion

        #region ContentType Helpers

        public static SPContentType GetContentTypeById(this SPWeb web, SPContentTypeId contentTypeId, bool throwIfNotFound)
        {
            try
            {
                return web.ContentTypes[contentTypeId];
            }
            catch (ArgumentException e)
            {
                if (throwIfNotFound)
                    throw new ArgumentException("Failed to find content type with id: '" + contentTypeId.ToString() + "'", "contentTypeId", e);
            }

            return null;
        }

        public static SPContentType GetContentTypeByName(this SPWeb web, string contentTypeName, bool throwIfNotFound)
        {
            try
            {
                return web.ContentTypes[contentTypeName];
            }
            catch (ArgumentException e)
            {
                if (throwIfNotFound)
                    throw new ArgumentException("Failed to find content type with name '" + contentTypeName + "'", "contentTypeName", e);
            }

            return null;
        }

        public static SPContentType GetContentTypeByIdOrName(this SPWeb web, SPContentTypeId? contentTypeId, string contentTypeName, bool throwIfNotFound)
        {
            if (contentTypeId.HasValue)
                return GetContentTypeById(web, contentTypeId.Value, throwIfNotFound);

            return GetContentTypeByName(web, contentTypeName, throwIfNotFound);
        }

        /// <summary>
        /// Gets an SPContentType provided its SPContentTypeId or name and throws an exception if not found
        /// </summary>
        public static SPContentType GetContentTypeByIdOrName(this SPWeb web, SPContentTypeId? contentTypeId, string contentTypeName)
        {
            return GetContentTypeByIdOrName(web, contentTypeId, contentTypeName, true);
        }

        public static string GetItemFormUrl(this SPListItem item, FormTypes formType)
        {
            // TODO: Currently only supports content types.  Need to add support for list based forms.

            return UrlUtils.EncodeSpaces(string.Format("{0}/{1}?List={2}&ID={3}",
                item.Web.Url.TrimEnd('/'),
                formType == FormTypes.Edit ? item.ContentType.EditFormUrl : item.ContentType.DisplayFormUrl,
                HttpUtility.UrlEncode(item.ParentList.ID.ToString()),
                item.ID)
                );
        }

        #endregion

        #region Field Helpers

        public static SPField GetFieldById(this SPFieldCollection fields, Guid fieldId, bool throwIfNotFound)
        {
            try
            {
                return fields[fieldId];
            }
            catch (ArgumentException e)
            {
                if (throwIfNotFound)
                    throw new ArgumentException("Failed to find field with id '" + fieldId.ToString() + "'", "fieldId", e);
            }

            return null;
        }

        public static SPField GetFieldByName(this SPFieldCollection fields, string fieldName, bool throwIfNotfound)
        {
            try
            {
                // or fields[fieldName];
                return fields.GetField(fieldName);
            }
            catch (ArgumentException e)
            {
                if (throwIfNotfound)
                    throw new ArgumentException("Failed to find field with name '" + fieldName + "'", "fieldName", e);
            }

            return null;
        }

        public static SPField GetFieldByIdOrName(this SPFieldCollection fields, Guid? fieldId, string fieldName)
        {
            return GetFieldByIdOrName(fields, fieldId, fieldName, true);
        }

        public static SPField GetFieldByIdOrName(this SPFieldCollection fields, Guid? fieldId, string fieldName, bool throwIfNotFound)
        {
            if (fieldId.HasValue)
                return GetFieldById(fields, fieldId.Value, throwIfNotFound);

            return GetFieldByName(fields, fieldName, throwIfNotFound);
        }

        #endregion

        #region Group Helpers

        public static SPGroup GetGroupById(this SPWeb web, int groupId)
        {
            return web.AssociatedGroups.SingleOrDefault(g => g.ID == groupId);
        }

        public static SPGroup GetGroup(this SPWeb web, string groupName)
        {
            return web.AssociatedGroups
                .SingleOrDefault(g => g.Name == groupName);
        }

        #endregion

        #region Feature Helpers

        public static bool IsActivated(this SPFeatureCollection features, Guid featureId)
        {
            return features.Cast<SPFeature>().FirstOrDefault(f => f.DefinitionId.Equals(featureId)) != null;
        }

        public static void EnsureActivated(this SPFeatureCollection features, Guid featureId)
        {
            if (!IsActivated(features, featureId))
                features.Add(featureId);
        }

        public static void EnsureDeactivated(this SPFeatureCollection features, Guid featureId)
        {
            if (IsActivated(features, featureId))
                features.Remove(featureId);
        }

        public static void EnsureFeaturesActivatedInAllChildren(this SPWebApplication webApp, bool throwExceptions, params Guid[] featureIds)
        {
            ToggleFeatureActivationsInAllChildren(webApp, true, throwExceptions, featureIds);
        }

        public static void EnsureFeaturesDeactivatedInAllChildren(this SPWebApplication webApp, bool throwExceptions, params Guid[] featureIds)
        {
            ToggleFeatureActivationsInAllChildren(webApp, false, throwExceptions, featureIds);
        }

        public static void ToggleFeatureActivationsInAllChildren(this SPWebApplication webApp, bool activate, bool throwExceptions, params Guid[] featureIds)
        {
            BypassCatchAccessDenied(delegate()
            {
                SPSecurity.RunWithElevatedPrivileges(delegate()
                {
                    var siteFeatures = new List<Guid>();
                    var webFeatures = new List<Guid>();
                    var featureDefinitions = SPFarm.Local.FeatureDefinitions;

                    foreach (var featureId in featureIds)
                    {
                        try
                        {
                            var featureDefinition = featureDefinitions[featureId];
                            if (featureDefinition.Scope == SPFeatureScope.Site)
                                siteFeatures.Add(featureId);
                            else if (featureDefinition.Scope == SPFeatureScope.Web)
                                webFeatures.Add(featureId);
                        }
                        catch (Exception e)
                        {
                            //Log.Error("Error finding an installed feature definition with Id " + featureId.ToString(), e);
                            if (throwExceptions)
                                throw e;
                        }
                    }

                    if (siteFeatures.Count == 0 && webFeatures.Count == 0)
                        return;

                    foreach (SPSite unprivelegedSite in webApp.Sites)
                    {
                        var exceptionRaised = false;
                        try
                        {
                            unprivelegedSite.DoAsSystemUser(delegate(SPSite site)
                            {
                                foreach (var featureId in siteFeatures)
                                {
                                    try
                                    {
                                        if (activate)
                                            site.Features.EnsureActivated(featureId);
                                        else
                                            site.Features.EnsureDeactivated(featureId);

                                        //Log.DebugFormat("Successfully {0} feature {1} in site {2}", activate ? "activated" : "deactivated", featureId, site.Url);
                                    }
                                    catch (Exception e)
                                    {
                                        //Log.Error(string.Format("Error toggling feature with Id '{0}' on site at '{1}'", featureId, site.Url), e);
                                        exceptionRaised = true;
                                        if (throwExceptions)
                                            throw e;
                                    }
                                }

                                if (webFeatures.Count == 0)
                                    return;

                                try
                                {
                                    foreach (SPWeb web in site.AllWebs)
                                    {
                                        try
                                        {
                                            foreach (var featureId in webFeatures)
                                            {
                                                try
                                                {
                                                    if (activate)
                                                        web.Features.EnsureActivated(featureId);
                                                    else
                                                        web.Features.EnsureDeactivated(featureId);

                                                    //Log.DebugFormat("Successfully {0} feature {1} in web {2}", activate ? "activated" : "deactivated", featureId, web.Url);
                                                }
                                                catch (Exception e)
                                                {
                                                    //Log.Error(string.Format("Error toggling feature with Id '{0}' on web at '{1}'", featureId, web.Url), e);
                                                    exceptionRaised = true;
                                                    if (throwExceptions)
                                                        throw e;
                                                }
                                            }
                                        }
                                        finally
                                        {
                                            web.Dispose();
                                        }
                                    }
                                }
                                catch (Exception e)
                                {
                                    //Log.Error(string.Format("Error enumerating webs in site at '{0}'", site.Url), e);
                                    exceptionRaised = true;
                                    if (throwExceptions)
                                        throw e;
                                }
                            });
                        }
                        catch (Exception e)
                        {
                            if (!exceptionRaised)
                            {
                                //Log.Error(string.Format("Error accessing site at '{0}'", unprivelegedSite.Url), e);
                                if (throwExceptions)
                                    throw e;
                            }
                        }
                        finally
                        {
                            unprivelegedSite.Dispose();
                        }
                    }
                });
            });
        }

        #endregion

        /// <summary>
        /// Method to find out if a SharePoint installation is MOSS or WSS 3.0
        /// Requires permissions to the Farm FeatureDefinitions collection.
        /// http://www.muhimbi.com/blog/2009/05/how-to-reliably-detect-moss-or-wss-at.html
        /// </summary>
        public static bool IsMOSS()
        {
            var features = SPFarm.Local.FeatureDefinitions;
            return (
                features["OssNavigation"] != null &&
                features["Publishing"] != null
                );
        }

        public static bool IsCentralAdmin(this SPWeb web)
        {
            return web.IsRootWeb &&
                web.WebTemplate == "CENTRALADMIN" &&
                web.Site.WebApplication.IsAdministrationWebApplication;
        }

        public static bool IsMySite(this SPWeb web)
        {
            return web.WebTemplate == "SPSPERS";
        }

        public static bool IsMySiteHost(this SPWeb web)
        {
            return web.WebTemplate == "SPSMSITEHOST";
        }

        public static bool IsSP2010()
        {
            return SPFarm.Local.BuildVersion.Major == 14;
        }

        public static void RemoveWebPartsFromGallery(this SPWeb web, SPFeatureReceiverProperties properties)
        {
            if (web.IsRootWeb)
            {
                var elements = properties.Definition.GetElementDefinitions(CultureInfo.InvariantCulture);
                var webparts = elements.Cast<SPElementDefinition>()
                    .SelectMany(e => e.XmlDefinition.ChildNodes.Cast<XmlElement>()
                        .Where(n => n.Name.Equals("File"))
                        .Select(n => n.Attributes["Url"].Value)
                        )
                    .ToList();

                var wpGallery = web.Lists["Web Part Gallery"];

                var galleryItems = wpGallery.Items.Cast<SPListItem>()
                    .Where(li => webparts.Contains(li.File.Name))
                    .ToList();

                for (int i = galleryItems.Count - 1; i >= 0; i--)
                {
                    var item = galleryItems[i];
                    item.Delete();
                }
            }
        }

        /// <summary>
        /// Ensures a script manager control has been register in the page's form.
        /// This should only be called during the Init lifecycle method of a page
        /// or control.
        /// </summary>
        /// <param name="page">The current page.</param>
        public static void EnsureScriptManager(this Page page)
        {
            if (page != null && ScriptManager.GetCurrent(page) == null)
            {
                page.Form.Controls.AddAt(0, new ScriptManager());
            }
        }
    }
}
